Fix: match adk-python when synthesizing a missing OpenAPI operationId - #800
Open
AmaadMartin wants to merge 2 commits into
Open
Fix: match adk-python when synthesizing a missing OpenAPI operationId#800AmaadMartin wants to merge 2 commits into
AmaadMartin wants to merge 2 commits into
Conversation
added 2 commits
August 8, 2026 03:39
An OpenAPI operation may omit operationId. adk-js synthesized
`${method}_${path}` with per-character punctuation replacement, while
adk-python synthesizes _to_snake_case(f"{path}_{method}"). The same spec
therefore produced different tool names in the two SDKs.
Add a shared snakeCase helper that mirrors _to_snake_case in
src/google/adk/tools/_gemini_schema_util.py, and use it for synthesis.
An operation that declares an operationId keeps it verbatim.
…eCase Step 1 already collapses each run of non-alphanumerics to a single underscore, and an underscore is itself non-alphanumeric. Steps 2 and 3 only insert an underscore between two alphanumeric characters. The repeated-underscore pass could therefore never match. Output is unchanged, verified over 569904 inputs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
n/a
Problem: An OpenAPI spec may omit
operationId. Both SDKs synthesize one, but they synthesize different strings, so the same spec produces different tool names in TypeScript and Python. adk-js builds the id as method-then-path and replaces each non-alphanumeric character with its own underscore; adk-python builds it as_to_snake_case(f"{path}_{method}"). A user who ports an agent, an eval set, or a tool-filter list between the SDKs finds the tool names do not match.Solution: Add an internal
snakeCase(text)tocore/src/utils/case_utils.tsand use it to synthesize the id from path-then-method. The helper reproduces the output of_to_snake_casein adk-python (src/google/adk/tools/_gemini_schema_util.py), so the two SDKs agree character for character. An operation that declares anoperationIdkeeps it verbatim; theif (!operation.operationId)guard is untouched. Parity wins over local convention here because the tool name is observable across the language boundary.Synthesized names, before and after:
/testgetget__testtest_get/users/{id}getget__users__id_users_id_get/userProfiles/{userId}getget__userProfiles__userId_user_profiles_user_id_get/pets/{petId}/photosdeletedelete__pets__petId__photospets_pet_id_photos_delete/v1/API-Keysputput__v1_API_Keysv1_api_keys_put/getget__getThis is a behaviour change. It affects only specs that omit
operationIdon at least one operation; a spec that names every operation is unaffected, including both in-repo fixtures. For affected users the tool name changes, so a pinnedtoolFilterentry, an eval set, or a prompt that names the tool breaks. The old names are not a designed format — they are the artefact of a per-character replace, with doubled and trailing underscores — and they diverge from the Python behaviour, which is the point of the change. No exported signature changes:snakeCaseis internal and is not added toindex.tsorcommon.ts.getParamNameis deliberately not touched.operation_parser.ts:46holds a second, cruder snake_case converter, and it is what turns the id into the final tool name. Routing it throughsnakeCasetoo would also rename the parameters and tools of specs that declare anoperationId(HTTPResponseCodebecomeshttp_response_codeinstead ofh_t_t_p_response_code), which is a wider break than this PR scopes itself to. #690 makes that change on its own.The helper has five steps, not six. adk-python's
_to_snake_casecollapses repeated underscores as its fifth step, but that step cannot fire: step 1 already collapses each run of non-alphanumerics to a single_, and_is itself non-alphanumeric, while steps 2 and 3 only ever insert an underscore between two alphanumeric characters. I checked it as well as asserted it: over every string of length up to 4 from a 16-character alphabet (lowercase, uppercase, digits, underscore,-,/,{,},.,%, space, newline, non-ASCII) plus 500k random strings of length up to 20 — 569,904 inputs — the five-step and six-step forms return the same result for every one. Test mutation 5 below pins the collapsing on step 1, where it actually happens.Collision check. I listed the 690 open PRs on this fork and read the diff of every adjacent one (#774, #748, #750, #690, #645, #605, #601, #462, #436, #389). None changes the synthesis line. #690 overlaps: it adds the same
snakeCasehelper and edits the same spec-parser test. I did not stack on it because its base isfeat/apihub-toolset-part2, a multi-PR stack, which would pull unrelated work into this diff. The two helpers differ only by the dead step described above and return identical output, so whichever lands second should keep this five-step body. The expected names in this PR hold both before and after #690'sgetParamNamechange.Testing Plan
Unit Tests:
Added: a
snakeCasetable incore/test/utils/case_utils_test.ts(11 rows, each row the output of adk-python's_to_snake_casefor that input), and three cases incore/test/tools/openapi_tool/openapi_spec_parser_test.ts— multi-operation synthesis, two methods on one path, and a declaredoperationIdthat must survive.openapi_toolset_integration_test.tspasses unedited. Both fixtures declare anoperationIdon every operation, so that suite is the evidence the change is inert for named specs.Coverage of the changed source, measured over the five files above:
core/src/utils/case_utils.ts100% statements, branches, functions and lines.openapi_spec_parser.ts99% lines; the two uncovered lines are 179-180 insanitizeSchemaTypes, which this PR does not touch.Two existing assertions changed, because they encode the bug.
openapi_spec_parser_test.ts:204pinnedget__users__id_andopenapi_toolset_test.ts:253pinnedget__test. Both now pin the adk-python value. The edits are one line each plus one comment; the tests are otherwise untouched. Every other test in the touched files is unchanged, and no test was deleted, skipped, or weakened.Proof the tests can fail. I ran the tests against five separate mutations. Each one failed.
expected 'get__users__id_' to be 'users_id_get'expected [ 'get__userProfiles__userId_', …(2) ] to deeply equal [ 'user_profiles_user_id_get', …(2) ]expected [ 'get__test', 'post__test' ] to deeply equal [ 'test_get', 'test_post' ]expected 'get__test' to be 'test_get'The "should not rewrite a declared operationId" case passes under this mutation by design: it pins the branch the mutation does not reach.
/^_+/g:expected 'leading_and_trailing_' to be 'leading_and_trailing'.expected 'httpresponse_code' to be 'http_response_code'.expected 'camelcase' to be 'camel_case'.+dropped from the first pattern, so runs stop collapsing:expected 'multiple___underscores' to be 'multiple_underscores'.Manual End-to-End (E2E) Tests:
Not applicable. The change is a string transform inside the spec parser, with no network, model, or filesystem access. To see it by hand, parse a spec whose operation omits
operationId:Checklist